package com.cta.service.core.impl;

import java.math.BigDecimal;
import java.math.RoundingMode;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

import javax.jms.JMSException;

import org.apache.commons.lang.StringUtils;
import org.joda.money.BigMoney;
import org.joda.time.DateTime;
import org.joda.time.Interval;
import org.joda.time.LocalDate;
import org.joda.time.LocalTime;
import org.joda.time.Period;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import com.cta.entity.AreaCodeRate;
import com.cta.entity.CallReport;
import com.cta.entity.Contact;
import com.cta.entity.Profile;
import com.cta.exception.InsertDataFail;
import com.cta.exception.MissingConfigException;
import com.cta.jms.MessageProducer;
import com.cta.jms.TransferObject;
import com.cta.log.RawDataLogger;
import com.cta.service.SiteService;
import com.cta.service.core.AbstractProcessor;
import com.cta.service.report.MasterDataService;
import com.cta.utils.CDRStringUtils;
import com.cta.utils.CodeLookupUtils;
import com.cta.view.json.CallDataJson;
import com.cta.view.model.CallData;
import com.cta.view.model.CallDuration;
import com.cta.view.model.ConfigModel;
import com.cta.view.model.ProfileModel;
import com.cta.view.model.discount.DiscountModel;
import com.cta.view.model.discount.HourDiscountModel;

@Service("processorCdr")
@Transactional(readOnly = true)
public class ProcessorImpl extends AbstractProcessor {

	private static final Logger log = LoggerFactory.getLogger(ProcessorImpl.class);

	@Autowired
	private MessageProducer producerService;

	@Autowired
	private MessageProducer producerConsoleService;

	@Autowired
	protected MasterDataService masterDataService;
	
	@Autowired
	protected SiteService siteService;

	@Override
	public ConfigModel lookup(CallData data) {
		log.info("====== " + data.getRawData() + " ======");
		log.info("Parsed data: " + data.toString());
		try {
			producerConsoleService.sendConsoleLogMessages("", data.toString());
		} catch (JMSException e) {
			log.error(">>>>>>>>>>>>>>>>>>>>>>> send to user console error");
		}
		ConfigModel configModel = null;
		Map<String, ConfigModel> config = CodeLookupUtils.getConfig(data.getTrunkId());
		if (config != null) {
			configModel = getBestMatch(data.getDialedNumber(), config);
		}
		return configModel;
	}

	@Override
	public String validate(CallData data, ConfigModel config) {
		if (config.isUseValidation()) {
			if (config.getShortNumber() > 0 && data.getDialedNumber().length() < config.getShortNumber()) {
				return "Dial number is shorter than config";
			}

			if (config.getLongNumber() > 0 && data.getDialedNumber().length() > config.getLongNumber()) {
				return "Dial number is too longer than config";
			}
		}
		log.info("validate step is ok ");
		return null;
	}

	@Override
	public BigMoney caculate(CallData data, ConfigModel config) throws Exception {
		// get rate value
		AreaCodeRate areaCodeRate = config.getAreaCodeRate();
		if (areaCodeRate == null) {
			throw new MissingConfigException(String.format("Can not found rate for code: %s", config.getAreaCode()));
		}
		// create start , end call date
		DateTime startDate = data.getDate();
		LocalTime startTime = data.getStartTime();
		Period duration = data.getDuration();
		startDate = startDate.plusHours(startTime.getHourOfDay()).plusMinutes(startTime.getMinuteOfHour())
				.plusSeconds(startTime.getSecondOfMinute());
		DateTime endDate = startDate.plus(duration);

		// should be valid call block here
		Interval i = new Interval(startDate, endDate);
		long curDuration = i.toDurationMillis() / 1000, realDuration = 0;
		log.info(String.format("startcall: %s, endCall: %s, duration: %ss", startDate, endDate,
				curDuration));
		if (curDuration < config.getFirstBlock()) {
			// fix duration
			realDuration = config.getFirstBlock();
		} else {
			// check continue block
			long secondBlock = curDuration - config.getFirstBlock();
			long remain = secondBlock % config.getSecondBlock();
			if (remain != 0) {
				secondBlock = secondBlock + (config.getSecondBlock() - remain);
			}
			realDuration = config.getFirstBlock() + secondBlock;
		}
		data.setDurationTime(realDuration);
		if (curDuration != realDuration) {
			// recaculate start date and enddate
			endDate = startDate.plus(realDuration * 1000);
			log.info(String.format("new startcall: %s, endCall: %s, duration: %ss", startDate,
					endDate, realDuration));
		}

		data.setStartDate(startDate);
		data.setEndDate(endDate);

		// get discount list for this call
		List<DiscountModel> discountRates = config.getDiscountRates();
		if (discountRates != null && discountRates.size() > 0) {
			for (DiscountModel discountModel : discountRates) {
				discountModel.hasDiscount(data);
			}
		}
		BigMoney price = null;
		if (data.hasDiscount()) {
			if (data.hasOnlyDayDiscount()) {
				data.setDisountType("Holiday");
				List<CallDuration> callDurs = calPriceWithHolidayDiscount(data);
				price = calPrice(config, areaCodeRate, callDurs);
				log.info(String.format("Discount type: %s -> Discount price: %s", data.getDisountType(), config.formatMoney(price)));
			} else if (data.hasOnlyHoursDiscount()) {
				data.setDisountType("Offpeak");
				List<CallDuration> callDurs = calPriceWithOffPeakDiscount(data);
				price = calPrice(config, areaCodeRate, callDurs);
				log.info(String.format("Discount type: %s -> Discount price: %s", data.getDisountType(), config.formatMoney(price)));
			} else {
				if (config.getDiscountType().equals("L")) {
					// should be get lowest price
					List<CallDuration> callDurs = calPriceWithHolidayDiscount(data);
					BigMoney price1 = calPrice(config, areaCodeRate, callDurs);
					/*log.info(String.format("holiday discount price: %s",
							config.formatMoney(price1)));*/
					callDurs = calPriceWithOffPeakDiscount(data);
					BigMoney price2 = calPrice(config, areaCodeRate, callDurs);
					/*log.info(String.format("hours discount price: %s",
							config.formatMoney(price2)));*/
					if (price1.compareTo(price2) <= 0) {
						data.setDisountType("Holiday");
						price = price1;
					} else {
						data.setDisountType("Offpeak");
						price = price2;
					}
					
					log.info(String.format("******* Discount type: %s -> Discount price: %s *******", data.getDisountType(), config.formatMoney(price)));
				} else if (config.getDiscountType().equals("H")) {
					data.setDisountType("Holiday");
					List<CallDuration> callDurs = calPriceWithHolidayDiscount(data);
					price = calPrice(config, areaCodeRate, callDurs);
					/*log.info(String.format("holiday discount price: %s",
							config.formatMoney(price)));*/
					log.info(String.format("******* Discount type: %s -> Discount price: %s *******", data.getDisountType(), config.formatMoney(price)));
				} else if (config.getDiscountType().equals("O")) {
					data.setDisountType("Offpeak");
					List<CallDuration> callDurs = calPriceWithOffPeakDiscount(data);
					price = calPrice(config, areaCodeRate, callDurs);
					/*log.info(String.format("hours discount price: %s",
							config.formatMoney(price)));*/
					log.info(String.format("******* Discount type: %s -> Discount price: %s *******", data.getDisountType(), config.formatMoney(price)));
				}
			}
		} else {
			data.setDisountType("None");
			price = calNormalPrice(config, areaCodeRate, data);
			log.info(String.format("Discount type: %s -> Discount price: %s", data.getDisountType(), config.formatMoney(price)));
		}
		//log.info(String.format("Final price: %s", config.formatMoney(price)));
		return price;
	}

	@Override
	public void endProccess(final CallReport data) throws InsertDataFail {
		// save to database
		String tableName = null;
		LocalDate localDate = data.getStartDate().toLocalDate();
		if (CodeLookupUtils.durationLock != null) {
			tableName = CodeLookupUtils.durationLock.lookup(localDate);
		}
		if (StringUtils.isEmpty(tableName)) {
			// create table
			try {
				CodeLookupUtils.durationLock = masterDataService.createTable(localDate);
			} catch (SQLException e) {
				throw new InsertDataFail("Can not create table or insert to master data", e);
			}
			tableName = CodeLookupUtils.durationLock.getTableName();
		}
		try {
			Contact contact = siteService.findContact(data.getEmployeeNum(), data.getCallingNum());
			if(contact != null){
				data.setUserId(contact.getId());
				data.setFullUserName(contact.getFullName());
			}
			masterDataService.insertData(data.getInsertSql(tableName));
		} catch (Exception e1) {
			throw new InsertDataFail("Can not insert data to slave table", e1);
		}
		log.info("insert to database ok");
		// convert log to client
		// move to another thread and get employee name
		executor.execute(new Runnable() {
			@Override
			public void run() {
				try {
					//lookup customer name and id base on authorization code or calling number
					CallDataJson json = new CallDataJson();
					json.setTrunkId(data.getTrunkId());
					json.setCallingNum(data.getCallingNum());
					json.setDialedNum(data.getDialedNum());
					json.setDestination(data.getDestination());
					json.setPrice(data.getPriceStr());
					json.setStartDate(data.getStartDate().toLocalDateTime().toString());
					json.setEndDate(data.getEndDate().toLocalDateTime().toString());
					json.setDuration(data.getDurationStr());
					json.setDiscount(data.getDiscount()); // serialize to string
					if(data.getUserId() != null){
						json.setEmployeeId(data.getUserId() + "");
						json.setEmployeeName(data.getFullUserName());
					}
					producerService.sendCallLogMessages(json.getEmployeeId(), CDRStringUtils.toString(json));
				} catch (JMSException e) {
					log.error("Error", e);
				}
			}
		});
		
	}

	private ConfigModel getBestMatch(String code, Map<String, ConfigModel> config) {
		ConfigModel result = null;
		for (int i = 1; i <= code.length(); i++) {
			String c = code.substring(0, i);
			ConfigModel configModel = config.get(c);
			if (configModel != null) {
				result = configModel;
			}
		}
		return result;
	}

	private BigMoney calNormalPrice(ConfigModel config, AreaCodeRate areaCodeRate, CallData data) {
		List<CallDuration> callDurs = new ArrayList<CallDuration>();
		CallDuration initDur = new CallDuration(
				(new Interval(data.getStartDate(), data.getEndDate())).toDurationMillis() / 1000, 0,
				data.getStartDate(), data.getEndDate());
		callDurs.add(initDur);
		return calPrice(config, areaCodeRate, callDurs);
	}

	private List<CallDuration> calPriceWithHolidayDiscount(CallData data) {
		List<CallDuration> callDurs = new ArrayList<CallDuration>();
		DateTime startDate = data.getStartDate();
		DateTime endDate = data.getEndDate();
		DateTime dateTimeMidd = startDate.withHourOfDay(23).withMinuteOfHour(59).withSecondOfMinute(59);
		if (endDate.compareTo(dateTimeMidd) > 0) {
			// should be split to 2 day
			Interval i = new Interval(startDate, dateTimeMidd);
			DiscountModel discountModel = data.getDayDiscounts().get(startDate.toLocalDate().withYear(0).toString());
			int discountValue = 0;
			if (discountModel != null) {
				discountValue = discountModel.getDiscountValue();
			}
			callDurs.add(new CallDuration(i.toDurationMillis() / 1000, discountValue));
			// call dur2
			i = new Interval(dateTimeMidd, endDate);
			discountValue = 0;
			discountModel = data.getDayDiscounts().get(endDate.toLocalDate().withYear(0).toString());
			if (discountModel != null) {
				discountValue = discountModel.getDiscountValue();
			}
			callDurs.add(new CallDuration(i.toDurationMillis() / 1000, discountValue));
		} else {
			Interval i = new Interval(startDate, endDate);
			DiscountModel discountModel = data.getDayDiscounts().get(startDate.toLocalDate().withYear(0).toString());
			int discountValue = 0;
			if (discountModel != null) {
				discountValue = discountModel.getDiscountValue();
			}
			callDurs.add(new CallDuration(i.toDurationMillis() / 1000, discountValue));
		}
		//log.info(String.format(">>>>>>>>>>>>>>>>>>>>>>> calculate with holiday discount: %s", callDurs.size()));
		return callDurs;
	}

	private List<CallDuration> calPriceWithOffPeakDiscount(CallData data) {
		Map<String, DiscountModel> hourDiscounts = data.getHourDiscounts();
		List<CallDuration> callDurs = new ArrayList<CallDuration>();
		CallDuration initDur = new CallDuration(
				(new Interval(data.getStartDate(), data.getEndDate())).toDurationMillis() / 1000, 0,
				data.getStartDate(), data.getEndDate());
		callDurs.add(initDur);
		for (DiscountModel hourDiscount : hourDiscounts.values()) {
			List<CallDuration> tempDurs = new ArrayList<CallDuration>();
			for (CallDuration callDuration : callDurs) {
				DateTime start = callDuration.getStart();
				DateTime end = callDuration.getEnd();
				HourDiscountModel hDis = (HourDiscountModel) hourDiscount;
				DateTime dateFrom = hDis.getDateFrom();
				DateTime dateTo = hDis.getDateTo();

				if (start.compareTo(dateFrom) < 0 && end.compareTo(dateTo) <= 0) {
					// non discount
					Interval i = new Interval(start, dateFrom);
					tempDurs.add(new CallDuration(i.toDurationMillis() / 1000, 0, start, dateFrom));
					// discount
					i = new Interval(dateFrom, end);
					tempDurs.add(new CallDuration(i.toDurationMillis() / 1000, hourDiscount.getDiscountValue(),
							dateFrom, end));
				} else if (start.compareTo(dateFrom) >= 0 && end.compareTo(dateTo) <= 0) {
					Interval i = new Interval(start, end);
					tempDurs.add(new CallDuration(i.toDurationMillis() / 1000, hourDiscount.getDiscountValue(), start,
							end));
				} else if (start.compareTo(dateTo) < 0 && end.compareTo(dateTo) > 0) {
					// discount
					Interval i = new Interval(start, dateTo);
					tempDurs.add(new CallDuration(i.toDurationMillis() / 1000, hourDiscount.getDiscountValue(), start,
							dateTo));
					// discount
					i = new Interval(dateTo, end);
					tempDurs.add(new CallDuration(i.toDurationMillis() / 1000, 0, dateTo, end));
				} else {
					// keep old duration if no split
					tempDurs.add(callDuration);
				}
				// renew call duration
				callDurs = tempDurs;
			}
		}
		return callDurs;
	}

	private BigMoney calPrice(ConfigModel config, AreaCodeRate areaCodeRate, List<CallDuration> callDurs) {
		if (callDurs.size() <= 0) {
			return null;
		}
		BigMoney firstPrice = BigMoney.ofScale(config.getCurrencyUnit(), new BigDecimal(0), 2, RoundingMode.HALF_DOWN);
		BigMoney secondPrice = BigMoney.ofScale(config.getCurrencyUnit(), new BigDecimal(0), 2, RoundingMode.HALF_DOWN);
		float firstBlock = 0, secondBlock = 0;
		for (CallDuration callDuration : callDurs) {
			//log.info(String.format("Duration >>> %s -> discount: %s", callDuration.getDuration(),
			//		callDuration.getDiscountValue()));
			if (firstBlock < config.getFirstBlock()) {
				if ((config.getFirstBlock() - firstBlock) <= callDuration.getDuration()) {
					firstBlock = config.getFirstBlock() - firstBlock;
					firstPrice = firstPrice.plus((firstBlock / config.getFirstBlock())
							* areaCodeRate.getFirstBlockValue() * (1 - callDuration.getDiscountValue() / 100));
					secondBlock = callDuration.getDuration() - firstBlock;
					if (secondBlock > 0) {
						secondPrice = secondPrice.plus((secondBlock / config.getSecondBlock())
								* areaCodeRate.getSecondBlockValue() * (1 - callDuration.getDiscountValue() / 100));
					}
				} else {
					// not enough for first block
					firstBlock = callDuration.getDuration();
					firstPrice = firstPrice.plus((firstBlock / config.getFirstBlock())
							* areaCodeRate.getFirstBlockValue() * (1 - callDuration.getDiscountValue() / 100));
					secondBlock = 0;
				}
			} else {
				secondBlock = callDuration.getDuration();
				secondPrice = secondPrice.plus((secondBlock / config.getSecondBlock())
						* areaCodeRate.getSecondBlockValue() * (1 - callDuration.getDiscountValue() / 100));
			}
		}
		return BigMoney.ofScale(config.getCurrencyUnit(), firstPrice.plus(secondPrice).getAmount(), 2,
				RoundingMode.HALF_DOWN);
	}

	
	
	public CallReport onMessage(TransferObject msg) throws JMSException {
		try {
			String data = msg.getRawData();
			RawDataLogger.log(data);
			// get config
			ProfileModel profileConfig = CodeLookupUtils.getProfile(msg.getPbxId());
			if (profileConfig != null && StringUtils.isNotEmpty(data)) {
				if (profileConfig.getLength() == data.trim().length()) {
					data = data.trim();
					int i = 0, e = 0;
					CallData callData = new CallData(data);
					for (Profile profile : profileConfig.getMapProfiles()) {
						e = i + profile.getNumOfBytes();
						if (data.length() >= e) {
							String val = data.substring(i, e);
							// ignore space character
							i = e + profile.getSpace();
							if (ProfileModel.isStartDate(profile)) {
								DateTime dateTime = new DateTime(profileConfig.getDateFormat().parse(val.trim()).getTime());
								callData.addData(profile, dateTime);
							} else if (ProfileModel.isStartTime(profile)) {
								LocalTime localTime = LocalTime.fromDateFields(profileConfig.getTimeFormat().parse(
										val.trim()));
								callData.addData(profile, localTime);
							} else if (ProfileModel.isDuration(profile)) {
								LocalTime localTime = LocalTime.fromDateFields(profileConfig.getDurationFormat().parse(
										val.trim()));
								Period p = new Period(localTime.getHourOfDay(), localTime.getMinuteOfHour(), localTime.getSecondOfMinute(), 0);
								callData.addData(profile, p);
							}  else {
								//trim and may be cut just number value
								callData.addData(profile, val.trim());
							}
						}
					}
					callData.setPbxId(msg.getPbxId());
					process(callData);
				} else {
					String errorText = String.format(
							"Data is not valid( not match length)-> real data length (%s), config length (%s) for raw data: %s",
							data.length(), profileConfig.getLength(), data);
					log.error(errorText);
				}
			} else if(profileConfig == null) {
				log.error(String.format("can not get profile config for this pbx %s", msg.getPbxId()));
			} else {
				log.error(String.format("data invalid %s", data));
			}

		} catch (Exception e) {
			log.error("Error", e);
		} 
		return null;
	}
	
}
